react-linear-feedback 0.1.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,34 +2,42 @@
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/react-linear-feedback.svg)](https://www.npmjs.com/package/react-linear-feedback) [![license](https://img.shields.io/npm/l/react-linear-feedback.svg)](./LICENSE)
4
4
 
5
- **A drop-in React feedback widget that turns a drawn box + note into a Linear issue with an annotated screenshot.**
5
+ **A drop-in feedback widget that turns a drawn box + note into a Linear issue with an annotated screenshot.** Use it as a React component, or embed it on **any site** — Framer, Webflow, plain HTML — with a single script tag.
6
6
 
7
- A trusted user opens it with `?feedback`, **drags a box** over the page, picks a type (**Bug / Improvement**), writes a note — and it captures a screenshot and files a Linear issue. Framework-agnostic, self-contained styling, no design system required.
7
+ A user opens the widget, **drags a box** over the page, picks a type (**Bug / Improvement**), writes a note — and it captures a screenshot and files a Linear issue. Self-contained styling, no design system required.
8
8
 
9
9
  <!-- Demo: record a short GIF and drop it here → ![demo](docs/demo.gif) -->
10
10
  > _Draw a box anywhere → type a note → it lands in Linear with the annotated screenshot and page context._
11
11
 
12
+ - 🌍 **Three ways in** — a React component, a framework-agnostic `init()` import for any bundler app, or a ~23KB script tag for sites with no build step
12
13
  - 🪶 **Themeable in one prop** — no CSS import, no Tailwind, no design system; styles are injected at runtime
13
- - 🌍 **Works in any React app** — Next.js, Vite, Remix, CRA… (no `next` dependency; `"use client"` is built in)
14
14
  - 🖼️ **Annotated screenshots** via [`modern-screenshot`](https://github.com/qq15725/modern-screenshot) (handles Tailwind v4 / `oklch()`)
15
- - 🏷️ **Labels by name, self-healing** — resolved at request time, so recoloring/recreating a label in Linear won't break it; applied best-effort
16
- - 🔌 **Tiny server core** + Next.js & Node/Express adapters
15
+ - 🏷️ **Labels by name, self-healing** — resolved at request time (and cached), so recoloring/recreating a label in Linear won't break it; applied best-effort
16
+ - 🔌 **One Web-standard server handler** + Next.js, Node/Express, and Vite-dev adapters, with built-in CORS for cross-origin embeds
17
17
 
18
18
  ## Contents
19
19
 
20
- - [Install](#install) · [Quick start (Next.js)](#quick-start-nextjs) · [Vite / any backend](#use-with-vite--any-react-app) · [Linear setup](#linear-setup) · [Configuration](#configuration) · [Theming](#theming) · [Custom types](#custom-types) · [Security](#security) · [Troubleshooting](#troubleshooting) · [License](#license)
20
+ - [How it fits together](#how-it-fits-together) · [React apps (npm)](#react-apps-npm) · [Any framework (npm import)](#any-framework-npm-import) · [Any site (script tag)](#any-site-script-tag) · [The server handler](#the-server-handler) · [Linear setup](#linear-setup) · [Configuration](#configuration) · [Theming](#theming) · [Custom types](#custom-types) · [Security](#security) · [Troubleshooting](#troubleshooting) · [License](#license)
21
21
 
22
- ## Install
22
+ ## How it fits together
23
+
24
+ The widget (React component or script-tag embed) never talks to Linear directly — it POSTs to **your** endpoint, which holds the API key server-side and creates the issue:
25
+
26
+ ```
27
+ widget (browser) ──POST──▶ your endpoint (createWebHandler) ──▶ Linear API
28
+ ```
29
+
30
+ So every setup is two steps: mount the widget, and deploy the handler ([The server handler](#the-server-handler)).
31
+
32
+ ## React apps (npm)
23
33
 
24
34
  ```bash
25
35
  npm i react-linear-feedback
26
- # the server entry needs the Linear SDK (optional peer):
27
- npm i @linear/sdk
28
36
  ```
29
37
 
30
- `react` / `react-dom` are peer dependencies. `@linear/sdk` is an **optional** peeronly needed wherever you run the server handler. Do [Linear setup](#linear-setup) first to get your API key, team, and labels.
38
+ `react` / `react-dom` are peer dependencies. `@linear/sdk` ships as a dependencyit's imported only by the server entry, so it's tree-shaken out of client bundles.
31
39
 
32
- ## Quick start (Next.js, App Router)
40
+ ### Next.js (App Router)
33
41
 
34
42
  **1. Server route** — `app/api/feedback/route.ts`:
35
43
 
@@ -62,38 +70,169 @@ export default function RootLayout({ children }) {
62
70
 
63
71
  No `"use client"` needed — the package ships it, so you can mount `<FeedbackGate>` straight from a Server Component layout.
64
72
 
65
- **3. Turn it on.** The widget is **hidden by default**. Visit any page with **`?feedback`** (or `?feedback=1`) to enable it; a cookie remembers the choice for 90 days. `?feedback=0` turns it off.
73
+ **3. Turn it on.** `<FeedbackGate>` is **hidden by default**. Visit any page with **`?feedback`** (or `?feedback=1`) to enable it; a cookie remembers the choice for 90 days. `?feedback=0` turns it off. (Use `<FeedbackWidget>` instead for an always-visible button.)
66
74
 
67
- ## Use with Vite / any React app
75
+ ### Vite SPA
68
76
 
69
- Mount the widget and point it at your backend:
77
+ Mount the widget the same way; the handler runs as a serverless function in production and as a dev-server plugin locally:
70
78
 
71
79
  ```tsx
72
80
  import { FeedbackGate } from "react-linear-feedback/react";
73
81
 
74
- <FeedbackGate endpoint="https://api.example.com/feedback" brandColor="#7f56d9" />
82
+ <FeedbackGate brandColor="#7f56d9" />; // endpoint defaults to /api/feedback (same origin)
83
+ ```
84
+
85
+ **Production — a Vercel serverless function** at `api/feedback.ts`:
86
+
87
+ ```ts
88
+ import { createNodeHandler, cookieGate } from "react-linear-feedback/server";
89
+
90
+ export default createNodeHandler({
91
+ apiKey: process.env.LINEAR_API_KEY!,
92
+ teamId: process.env.LINEAR_TEAM_ID!,
93
+ authorize: cookieGate("wh_feedback"),
94
+ });
95
+ ```
96
+
97
+ Set `LINEAR_API_KEY` / `LINEAR_TEAM_ID` in your Vercel project's env (server-side — **not** `VITE_`-prefixed, so they never reach the bundle).
98
+
99
+ **Local dev — the Vite plugin**, so `vite dev` serves the same endpoint (without it, `POST /api/feedback` 404s locally):
100
+
101
+ ```ts
102
+ // vite.config.ts
103
+ import { defineConfig, loadEnv } from "vite";
104
+ import { linearFeedback } from "react-linear-feedback/vite";
105
+ import { cookieGate } from "react-linear-feedback/server";
106
+
107
+ export default defineConfig(({ mode }) => {
108
+ const env = loadEnv(mode, process.cwd(), ""); // reads .env (LINEAR_* are server-side, un-prefixed)
109
+ return {
110
+ plugins: [
111
+ linearFeedback({
112
+ apiKey: env.LINEAR_API_KEY,
113
+ teamId: env.LINEAR_TEAM_ID,
114
+ authorize: cookieGate("wh_feedback"),
115
+ }),
116
+ ],
117
+ };
118
+ });
119
+ ```
120
+
121
+ The plugin is dev-only (`apply: "serve"`) — it has no effect on the production build.
122
+
123
+ ## Any framework (npm import)
124
+
125
+ Not a React app? The same self-mounting widget is available as a plain ESM import — Vue, Svelte, Astro, Solid, vanilla TS, anything with a bundler. **Zero peer dependencies** (a compact React-compatible runtime is bundled inside, ~23KB gz):
126
+
127
+ ```ts
128
+ import { init, destroy } from "react-linear-feedback/embed";
129
+
130
+ init({
131
+ endpoint: "/api/feedback",
132
+ brandColor: "#7f56d9",
133
+ // mode: "gated", // ?feedback URL-param gate, like <FeedbackGate>
134
+ // token: "...", // for headerGate'd cross-origin endpoints
135
+ });
136
+
137
+ // destroy() unmounts it (e.g. SPA route teardown / HMR)
138
+ ```
139
+
140
+ `init()` takes the same options as the React component plus `mode` and `token` (see [Configuration](#configuration)). Calling it again replaces the existing instance.
141
+
142
+ React apps should prefer the [`<FeedbackGate>` component](#react-apps-npm) — it shares the page's React instead of shipping a second renderer.
143
+
144
+ ## Any site (script tag)
145
+
146
+ No npm, no build step, no React on the page — the embed bundle (~23KB gz, served from a CDN off the npm package) mounts the same widget on any site: Framer, Webflow, WordPress, plain HTML.
147
+
148
+ ```html
149
+ <script
150
+ src="https://cdn.jsdelivr.net/npm/react-linear-feedback@0.3.0/dist/embed/linear-feedback.js"
151
+ data-endpoint="https://your-app.example.com/api/feedback"
152
+ data-brand-color="#7f56d9"
153
+ data-token="your-embed-token"
154
+ defer
155
+ ></script>
156
+ ```
157
+
158
+ Pin the version in the URL (as above). Because the page and the endpoint are usually on **different origins**, the handler needs `allowedOrigins` set — see [The server handler](#the-server-handler).
159
+
160
+ It must be a classic `<script src>` tag (not `type="module"`, not injected via `innerHTML`) so the embed can read its own config; if you need dynamic injection, use `data-manual` + `init()` below.
161
+
162
+ ### Script-tag options
163
+
164
+ | Attribute | Maps to | Notes |
165
+ | --- | --- | --- |
166
+ | `data-endpoint` | `endpoint` | **Required in practice** — your deployed handler URL |
167
+ | `data-brand-color` | `brandColor` | Any CSS color |
168
+ | `data-position` | `position` | `bottom-right` (default) \| `bottom-left` \| `top-right` \| `top-left` \| `right` \| `left` |
169
+ | `data-fab-label` | `fabLabel` | Button text |
170
+ | `data-mode` | `mode` | `open` (default, always visible) \| `gated` (`?feedback` URL param + cookie, like `<FeedbackGate>`) |
171
+ | `data-token` | `token` | Sent as the `x-feedback-token` header — pair with `headerGate` on the server |
172
+ | `data-z-index` | `zIndex` | Override the widget's stacking context |
173
+ | `data-name-required` | `nameRequired` | `"false"` to skip the name prompt |
174
+ | `data-manual` | — | `"true"` disables auto-init; call `LinearFeedback.init()` yourself |
175
+
176
+ ### JS API
177
+
178
+ Options that aren't expressible as data attributes (like custom `types`) go through `window.lfbConfig` (set **before** the script tag; merged over the data attributes) or the imperative API:
179
+
180
+ ```html
181
+ <script>
182
+ window.lfbConfig = {
183
+ types: [
184
+ { id: "bug", label: "Bug", color: "#ef4444", icon: "bug" },
185
+ { id: "idea", label: "Idea", color: "#22c55e", icon: "improvement" },
186
+ ],
187
+ };
188
+ </script>
189
+ <script src="https://cdn.jsdelivr.net/npm/react-linear-feedback@0.3.0/dist/embed/linear-feedback.js" data-endpoint="..." defer></script>
190
+ ```
191
+
192
+ ```js
193
+ // With data-manual="true":
194
+ LinearFeedback.init({ endpoint: "https://...", brandColor: "#7f56d9", token: "..." });
195
+ LinearFeedback.destroy();
75
196
  ```
76
197
 
77
- Run the handler on any Node server (Express shown):
198
+ A runnable demo lives in [`examples/embed.html`](./examples/embed.html) + [`examples/dev-server.mjs`](./examples/dev-server.mjs).
199
+
200
+ ## The server handler
201
+
202
+ `createWebHandler` is the canonical handler — a Web-standard `(Request) => Promise<Response>` function. `createNextRoute` is the same function under its Next.js name, and `createNodeHandler` wraps it for `(req, res)`-style servers (Express, plain `node:http`, Vercel Node functions). All of them run on the **Node runtime** (the screenshot upload uses `Buffer`; Edge isn't supported).
203
+
204
+ ```ts
205
+ import { createWebHandler, headerGate } from "react-linear-feedback/server";
206
+
207
+ const handler = createWebHandler({
208
+ apiKey: process.env.LINEAR_API_KEY!,
209
+ teamId: process.env.LINEAR_TEAM_ID!,
210
+ // Required for script-tag embeds on other domains — enables CORS (preflight + headers):
211
+ allowedOrigins: ["https://your-marketing-site.com", "https://www.your-marketing-site.com"],
212
+ // Cookie gates can't cross origins — gate embeds with the shared token instead:
213
+ authorize: headerGate("your-embed-token"),
214
+ });
215
+ ```
216
+
217
+ Cross-origin example end-to-end: the script tag on `your-marketing-site.com` has `data-endpoint="https://your-app.example.com/api/feedback"` and `data-token="your-embed-token"`; the handler at that endpoint lists the marketing origin in `allowedOrigins` and gates with `headerGate("your-embed-token")`. Same-origin setups (widget and handler on one domain) need neither.
218
+
219
+ ### Express / other Node servers
220
+
221
+ `createNodeHandler` works with or without `express.json()`:
78
222
 
79
223
  ```ts
80
224
  import express from "express";
81
- import cors from "cors";
82
225
  import { createNodeHandler, cookieGate } from "react-linear-feedback/server";
83
226
 
84
227
  const app = express();
85
- // If the app and API are on different origins, CORS is required or the fetch is blocked:
86
- app.use("/feedback", cors({ origin: "https://your-site.com", credentials: true }));
87
- app.post("/feedback", createNodeHandler({
228
+ app.post("/api/feedback", createNodeHandler({
88
229
  apiKey: process.env.LINEAR_API_KEY!,
89
230
  teamId: process.env.LINEAR_TEAM_ID!,
90
- authorize: cookieGate("wh_feedback"), // optional — omit to leave the endpoint open
231
+ authorize: cookieGate("wh_feedback"),
91
232
  }));
92
233
  app.listen(8787);
93
234
  ```
94
235
 
95
- The handler reads the raw body itself, so it works with or without `express.json()`.
96
-
97
236
  ## Linear setup
98
237
 
99
238
  You need three things from Linear:
@@ -110,7 +249,7 @@ LINEAR_TEAM_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
110
249
 
111
250
  ## Configuration
112
251
 
113
- ### Widget props (`<FeedbackGate>` / `<FeedbackWidget>`)
252
+ ### Widget props (`<FeedbackGate>` / `<FeedbackWidget>` / `LinearFeedback.init`)
114
253
 
115
254
  Most apps only set `brandColor` and maybe `endpoint`.
116
255
 
@@ -118,32 +257,35 @@ Most apps only set `brandColor` and maybe `endpoint`.
118
257
  | --- | --- | --- |
119
258
  | `endpoint` | `/api/feedback` | Where the widget POSTs |
120
259
  | `brandColor` | `#6366f1` | FAB / active / focus color (sets the `--lfb-brand` CSS var) |
121
- | `position` | `bottom-right` | `bottom-right` \| `bottom-left` \| `top-right` \| `top-left` |
260
+ | `position` | `bottom-right` | `bottom-right` \| `bottom-left` \| `top-right` \| `top-left` \| `right` \| `left` (edge tabs: compact icon-only launcher) |
122
261
  | `types` | Bug, Improvement | `{ id, label, color, icon? }[]` shown in the composer |
123
262
  | `nameRequired` | `true` | Ask for a reporter name before the first submission (saved to localStorage, included in the issue) |
124
263
  | `nameStorageKey` | `wh_feedback_name` | localStorage key for the remembered name |
125
264
  | `fabLabel` | `Give feedback` | Floating button text |
265
+ | `zIndex` | `2147483640` | Stacking context for the widget's layers (sugar for `--lfb-z`, see [Theming](#theming)) |
266
+ | `requestHeaders` | — | Extra headers sent with the submission (the embed's `token` uses this) |
126
267
  | `urlParam` ¹ | `feedback` | Toggle param |
127
268
  | `cookieName` ¹ | `wh_feedback` | Enabled-state cookie name |
128
269
  | `cookieValue` ¹ | `1` | Cookie value when enabled |
129
270
  | `cookieMaxAgeSeconds` ¹ | `7776000` (90d) | Cookie lifetime |
130
271
 
131
- ¹ `<FeedbackGate>` only.
272
+ ¹ `<FeedbackGate>` only (and the embed's `mode: "gated"`).
132
273
 
133
- ### Server config (`createNextRoute` / `createNodeHandler` / `createFeedbackIssue`)
274
+ ### Server config (`createWebHandler` / `createNextRoute` / `createNodeHandler` / `createFeedbackIssue`)
134
275
 
135
276
  | Field | Description |
136
277
  | --- | --- |
137
278
  | `apiKey` | Linear personal API key (**server-side only**) |
138
279
  | `teamId` | Target team UUID |
139
280
  | `labels` | Optional `{ [typeId]: labelName }` map. Default: the type id **is** the label name (so type `bug` → label `bug`) |
140
- | `allowedOrigin` | Optional single-origin allowlist (adapters) |
141
- | `authorize(req)` | Optional gate; return `false` to reject. `cookieGate(name, value="1")` is provided |
281
+ | `labelCacheTtlMs` | How long label name→ID lookups are cached (default 10 min; `0` disables). Saves a Linear API round-trip per submission |
282
+ | `allowedOrigins` | Origins allowed to call the endpoint cross-origin (exact origins or `"*"`); enables CORS. Required for script-tag embeds on other domains |
283
+ | `authorize(req)` | Optional gate; return `false` to reject. `cookieGate(name, value="1")` and `headerGate(token, headerName="x-feedback-token")` are provided |
142
284
 
143
285
  Remap type ids to differently-named Linear labels:
144
286
 
145
287
  ```ts
146
- createNextRoute({
288
+ createWebHandler({
147
289
  apiKey, teamId,
148
290
  labels: { bug: "bug", idea: "feature-request" },
149
291
  });
@@ -151,9 +293,15 @@ createNextRoute({
151
293
 
152
294
  ## Theming
153
295
 
154
- Set `brandColor`, or override any CSS variable on `.lfb-doc-layer, .lfb-fixed-layer`:
296
+ Set `brandColor`, or override any CSS variable on `.lfb-root`:
155
297
  `--lfb-brand`, `--lfb-fg`, `--lfb-surface`, `--lfb-border`, `--lfb-radius`, `--lfb-rect`, `--lfb-z`, `--lfb-font`.
156
298
 
299
+ The widget defaults to a very high `--lfb-z` (`2147483640`) so it sits above app chrome. If it lands on top of your own modals or toasts, lower it — either with the `zIndex` prop / `data-z-index` attribute, or in CSS:
300
+
301
+ ```css
302
+ .lfb-root { --lfb-z: 40; }
303
+ ```
304
+
157
305
  ## Custom types
158
306
 
159
307
  ```tsx
@@ -165,25 +313,31 @@ Set `brandColor`, or override any CSS variable on `.lfb-doc-layer, .lfb-fixed-la
165
313
  />
166
314
  ```
167
315
 
168
- Each `type.id` is matched to a Linear label of the same name. If your label is named differently, map it on the server via `labels` (above). Built-in `icon` values: `"bug"`, `"improvement"`, `"dot"`.
316
+ Each `type.id` is matched to a Linear label of the same name. If your label is named differently, map it on the server via `labels` (above). Built-in `icon` values: `"bug"`, `"improvement"`, `"dot"`. For the script-tag embed, pass `types` via `window.lfbConfig` or `LinearFeedback.init()`.
169
317
 
170
318
  ## Security
171
319
 
172
320
  ⚠️ **The endpoint creates Linear issues**, so it's effectively write access to your tracker, and it's **open by default**. Before shipping on a public page:
173
321
 
174
- - **Gate it** with `authorize` (e.g. `cookieGate`, or your own session check).
175
- - **Restrict origin** with `allowedOrigin: "https://your-site.com"`.
322
+ - **Gate it** with `authorize` (e.g. `cookieGate`, `headerGate`, or your own session check).
323
+ - **Restrict origins** with `allowedOrigins: ["https://your-site.com"]`.
176
324
  - **Rate-limit** if the page is public (e.g. per-IP).
177
325
 
326
+ ### Gating an embedded (cross-origin) widget
327
+
328
+ `cookieGate` only works **same-origin**: the gate cookie is written on the page's origin, so the browser never sends it with a cross-origin submission. For script-tag embeds, use the shared token instead — `data-token="..."` on the script tag plus `authorize: headerGate("...")` on the handler. The token is visible in the page source, so treat it as a **tripwire against drive-by spam, not authentication**; combine it with `allowedOrigins` and rate limiting.
329
+
178
330
  Your `LINEAR_API_KEY` stays server-side; the package never ships it to the browser. Screenshots are uploaded to Linear's **private** asset storage — they render inside the issue, but the URL needs a fresh signed token to fetch elsewhere (it can't be hot-linked). If a screenshot upload fails, the issue is still created without it.
179
331
 
180
332
  ## Troubleshooting
181
333
 
182
334
  Submissions never throw in the UI — failures are logged to the browser console with a `[feedback]` prefix, and server errors return a JSON `message`. If issues aren't being created, check:
183
335
 
184
- - `endpoint` points at your route, and `LINEAR_API_KEY` / `LINEAR_TEAM_ID` are set.
185
- - **CORS** is configured when the app and API are on different origins.
336
+ - `endpoint` points at your route, and `LINEAR_API_KEY` / `LINEAR_TEAM_ID` are set (the handler logs a `[feedback]` warning at startup when they're missing).
337
+ - A rejected `authorize` returns **404 by design** — the endpoint is hidden, not just forbidden. If you're debugging your gate and seeing 404s, it's not a routing problem.
338
+ - **CORS**: cross-origin pages (script-tag embeds) need their origin in `allowedOrigins`, and the browser must reach the endpoint with both `OPTIONS` and `POST`.
186
339
  - `runtime = "nodejs"` is set on the Next.js route (Edge has no `Buffer`).
340
+ - On a **Vite SPA**, `POST /api/feedback` 404s under `vite dev` unless you add the [`linearFeedback` Vite plugin](#vite-spa) (or run `vercel dev`). In production it's served by your deployed function.
187
341
  - The expected Linear **labels exist** (otherwise the issue is created without a label, with a warning).
188
342
 
189
343
  ## License
@@ -0,0 +1,48 @@
1
+ /** Built-in inline icon keys usable on a type option. */
2
+ type FeedbackIconName = "bug" | "improvement" | "dot";
3
+ /** A selectable issue type shown in the composer's segmented control. */
4
+ type FeedbackTypeOption = {
5
+ /** Stable id sent to the server and mapped to a Linear label name. */
6
+ id: string;
7
+ /** Human label shown in the UI and used in the issue title (e.g. "Bug"). */
8
+ label: string;
9
+ /** Swatch background color, any CSS color. */
10
+ color: string;
11
+ /** Optional built-in icon for the swatch. */
12
+ icon?: FeedbackIconName;
13
+ };
14
+
15
+ type FeedbackPosition = "bottom-right" | "bottom-left" | "top-right" | "top-left"
16
+ /** Edge tabs: compact, icon-only launcher flush to the side, vertically centered. */
17
+ | "right" | "left";
18
+ type FeedbackWidgetProps = {
19
+ /** Endpoint that runs the server handler (default "/api/feedback"). */
20
+ endpoint?: string;
21
+ /** Brand color for the FAB, active states and focus rings (any CSS color). */
22
+ brandColor?: string;
23
+ /** Corner for the floating button (default "bottom-right"). */
24
+ position?: FeedbackPosition;
25
+ /** Selectable issue types (default Bug / Improvement). */
26
+ types?: FeedbackTypeOption[];
27
+ /** Ask for a reporter name before the first submission (default true). */
28
+ nameRequired?: boolean;
29
+ /** localStorage key for the remembered name. */
30
+ nameStorageKey?: string;
31
+ /** Label on the floating button (default "Give feedback"). */
32
+ fabLabel?: string;
33
+ /** Stacking context for the widget's layers — sugar for the `--lfb-z` CSS variable. */
34
+ zIndex?: number;
35
+ /** Extra headers sent with the submission (e.g. x-feedback-token for `headerGate`). */
36
+ requestHeaders?: Record<string, string>;
37
+ };
38
+
39
+ type LinearFeedbackConfig = FeedbackWidgetProps & {
40
+ /** "open" (default): widget always visible. "gated": enabled via ?feedback URL param + cookie. */
41
+ mode?: "open" | "gated";
42
+ /** Shared secret sent as the x-feedback-token header — pair with `headerGate` on the server. */
43
+ token?: string;
44
+ };
45
+ declare function init(config?: LinearFeedbackConfig): void;
46
+ declare function destroy(): void;
47
+
48
+ export { type LinearFeedbackConfig, destroy, init };